SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
7.8 KB · 166 lines tsx
Raw Blame History
1import Link from "next/link";2import { notFound } from "next/navigation";3import { ArrowLeft } from "lucide-react";4import { requireAdmin } from "@/lib/session";5import { CONCRETE_NETWORKS, PROVIDER_IDS, getDomainProfile } from "@/lib/queries/admin";6import { formatDate, formatMs, formatNumber, formatPercent, formatUsd } from "@/lib/format";7import { PageHeader } from "@/components/ui/page-header";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { DateCell, KV, Mono, NetworkBadge, Panel, ProviderBadge, numCell } from "@/components/admin/primitives";13import { DomainPolicyEditor, ResetDomainStatsButton } from "@/components/admin/domain-policy-editor";1415export const dynamic = "force-dynamic";1617export default async function AdminDomainDetail({ params }: { params: Promise<{ domain: string }> }) {18  await requireAdmin();19  const { domain: raw } = await params;20  const domain = decodeURIComponent(raw).toLowerCase();21  const d = await getDomainProfile(domain);22  if (!d) notFound();23  const p = d.profile;24  const routes = Object.entries(p.routeStats ?? {}).sort((a, b) => b[1].n - a[1].n);25  const availableKeys = Array.from(new Set([...routes.map(([k]) => k), ...PROVIDER_IDS.flatMap((pr) => CONCRETE_NETWORKS.map((n) => `${pr}:${n}`))]));2627  return (28    <>29      <div className="mb-4">30        <Button asChild variant="ghost" size="sm" className="-ml-2">31          <Link href="/admin/domains">32            <ArrowLeft className="size-3.5" /> Domains33          </Link>34        </Button>35      </div>36      <PageHeader37        eyebrow="Domain profile"38        title={<Mono className="text-[20px]">{p.domain}</Mono>}39        description={`Learned since ${formatDate(p.updatedAt)} · last seen ${formatDate(p.lastSeenAt)}`}40        actions={41          <>42            <Button asChild variant="outline" size="sm">43              <Link href={`/admin/requests?domain=${encodeURIComponent(p.domain)}`}>Explore requests</Link>44            </Button>45            <ResetDomainStatsButton domain={p.domain} />46          </>47        }48      />4950      <StatGrid cols={6} className="mb-6">51        <Stat label="Requests" value={formatNumber(p.requests)} />52        <Stat label="Success" value={formatPercent(p.requests ? (p.successes / p.requests) * 100 : null)} />53        <Stat label="Blocked" value={formatPercent(p.requests ? (p.blocks / p.requests) * 100 : null)} hint={`${p.blocks} blocks`} />54        <Stat label="Captcha" value={formatPercent(p.requests ? (p.captchas / p.requests) * 100 : null)} hint={`${p.captchas} seen`} />55        <Stat label="Browser required" value={formatNumber(p.browserRequired)} hint="signals seen" />56        <Stat label="Avg latency" value={formatMs(p.avgLatencyMs)} />57      </StatGrid>5859      <div className="grid gap-6 lg:grid-cols-2">60        <Panel title="Route statistics" description="Per provider:network key, as used by the routing score (35 % success, 15 % latency)." flush>61          <Table>62            <TableHeader>63              <TableRow>64                <TableHead>Route</TableHead>65                <TableHead className="text-right">n</TableHead>66                <TableHead className="text-right">ok</TableHead>67                <TableHead className="text-right">Success</TableHead>68                <TableHead className="text-right">Blocked</TableHead>69                <TableHead className="text-right">Avg lat.</TableHead>70                <TableHead className="text-right">Cost</TableHead>71                <TableHead className="text-right">Cost / success</TableHead>72              </TableRow>73            </TableHeader>74            <TableBody>75              {routes.length === 0 ? (76                <TableEmpty colSpan={8}>No route statistics yet.</TableEmpty>77              ) : (78                routes.map(([key, s]) => {79                  const [prov, net] = key.split(":");80                  return (81                    <TableRow key={key}>82                      <TableCell>83                        <span className="inline-flex items-center gap-1.5">84                          <ProviderBadge id={prov} />85                          <NetworkBadge network={net} />86                        </span>87                      </TableCell>88                      <TableCell className={numCell}>{s.n}</TableCell>89                      <TableCell className={numCell}>{s.ok}</TableCell>90                      <TableCell className={numCell}>{formatPercent(s.n ? (s.ok / s.n) * 100 : null)}</TableCell>91                      <TableCell className={numCell}>{formatPercent(s.n ? (s.blocked / s.n) * 100 : null)}</TableCell>92                      <TableCell className={numCell}>{formatMs(s.lat)}</TableCell>93                      <TableCell className={numCell}>{formatUsd(s.cost, true)}</TableCell>94                      <TableCell className={`${numCell} font-semibold`}>{formatUsd(s.ok ? s.cost / s.ok : null, true)}</TableCell>95                    </TableRow>96                  );97                })98              )}99            </TableBody>100          </Table>101        </Panel>102        <div className="grid gap-6">103          <Panel title="Policy" description="Admin overrides applied before scoring. Saved immediately; takes effect on the next request.">104            <DomainPolicyEditor domain={p.domain} policy={p.policy ?? null} availableKeys={availableKeys} />105          </Panel>106          <Panel title="Learned preference">107            <KV108              cols={2}109              items={[110                { label: "Preferred provider", value: <ProviderBadge id={p.preferredProvider} /> },111                { label: "Preferred network", value: <NetworkBadge network={p.preferredNetwork} /> },112                { label: "Policy JSON", value: p.policy ? JSON.stringify(p.policy) : "none", mono: true, span: true },113              ]}114            />115          </Panel>116        </div>117      </div>118119      <Panel title="Recent requests (7d)" flush className="mt-6">120        <Table>121          <TableHeader>122            <TableRow>123              <TableHead>Request</TableHead>124              <TableHead>Organization</TableHead>125              <TableHead>Status</TableHead>126              <TableHead>Network</TableHead>127              <TableHead className="text-right">Attempts</TableHead>128              <TableHead className="text-right">Latency</TableHead>129              <TableHead className="text-right">When</TableHead>130            </TableRow>131          </TableHeader>132          <TableBody>133            {d.recent.length === 0 ? (134              <TableEmpty colSpan={7}>No requests in the last 7 days.</TableEmpty>135            ) : (136              d.recent.map((r) => (137                <TableRow key={r.id}>138                  <TableCell>139                    <Link href={`/admin/requests/${r.id}`} className="hover:underline">140                      <Mono>{r.id}</Mono>141                    </Link>142                  </TableCell>143                  <TableCell>{r.orgName ?? "—"}</TableCell>144                  <TableCell>145                    <StatusBadge status={r.status} />146                    {r.errorCode ? <Badge variant="outline" className="ml-1 font-mono">{r.errorCode}</Badge> : null}147                    {r.httpStatus ? <Mono className="ml-1 text-fg-muted">{r.httpStatus}</Mono> : null}148                  </TableCell>149                  <TableCell>150                    <NetworkBadge network={r.network} />151                  </TableCell>152                  <TableCell className={numCell}>{r.attempts}</TableCell>153                  <TableCell className={numCell}>{formatMs(r.latencyMs)}</TableCell>154                  <TableCell className="text-right">155                    <DateCell value={r.createdAt} />156                  </TableCell>157                </TableRow>158              ))159            )}160          </TableBody>161        </Table>162      </Panel>163    </>164  );165}166